[wasm] Use ordinary struct alignment for Int128/UInt128 on wasm - #131421
[wasm] Use ordinary struct alignment for Int128/UInt128 on wasm#131421lewing wants to merge 2 commits into
Conversation
Int128/UInt128 were given a 16-byte alignment requirement on wasm by both the VM
and crossgen2, annotated "sizeof(v128)". Int128 is not a v128: the only 128-bit
value type in WebAssembly is v128, and WasmLowering.IsWasmV128Type recognizes
only Vector128<T> and a 128-bit Vector<T>, so Int128 uses the generic
by-reference struct ABI.
Every other alignment override in CheckForSystemTypes exists because the managed
type corresponds to a fundamental data type in the target ABI (Vector128<T> to
__m128, and so on). Int128 has no counterpart on wasm, so it takes ordinary
struct alignment, which is the 8 its two ulong fields produce. On the crossgen2
side that means routing wasm through the existing 32-bit path.
The 16 was observable as wrong codegen at the interpreter-to-R2R boundary. Wasm
signature encoding spells a by-reference struct argument as S<N>, and
RaiseSignature resolves S16 through GetCachedStructOfSize, which keys only on
size and keeps the first struct seen at that size. The thunk therefore used that
struct's 8-byte alignment while the interpreter used Int128's 16, placing the
argument at argsBase+24 instead of argsBase+32. Int128.Equals and CompareTo then
read a shifted value while operator== was unaffected.
This also makes S<N> sound rather than accidentally correct: single-field structs
wrapping a v128 are unwrapped by LowerType and encode as V, and a v128 field is
the only source of 16-byte alignment on wasm, so once Int128 takes its natural
alignment nothing reaching S<N> requires more than 8.
The VM and crossgen2 must change together or managed field layout desyncs.
Validated on browser wasm against builds with and without the change, rebuilding
both corerun and the R2R composite for each:
Int128 repro, R2R on: before, Equals=False and CompareTo=+/-1 on equal values;
after, all correct. R2R off correct in both.
System.Text.Json, the Int128-bearing classes, 1496 tests, on a branch carrying
the other outstanding wasm R2R fixes: before, R2R-on 20 failed (15 Int128);
after, 2 failed (0 Int128). Those 2 also fail with R2R off, so they are
pre-existing and unrelated.
Microsoft.Bcl.Memory 550/550 and Unsafe 128/128, R2R on and off.
Encoding Int128 as V instead does not work: V selects a different calling
convention, passing the value in a wasm v128 local rather than by reference, and
the JIT has no notion of Int128, so R2R code faults with "function signature
mismatch".
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke |
|
I believe clang sets |
|
cc @dotnet/wasm-contrib |
There was a problem hiding this comment.
Pull request overview
This PR changes how System.Int128/System.UInt128 alignment is treated on WebAssembly so that wasm uses ordinary struct alignment (8) instead of a forced 16-byte alignment, keeping VM and crossgen2/R2R field layout consistent and avoiding ABI mismatches at interpreter↔R2R boundaries.
Changes:
- In CoreCLR VM
CheckForSystemTypes, switch wasmInt128/UInt128alignment requirement from 16 to 8. - In crossgen2 field layout, treat
TargetArchitecture.Wasm32like existing 32-bit ARM behavior by using the standard metadata layout engine forInt128/UInt128.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/coreclr/vm/methodtablebuilder.cpp | Updates wasm Int128/UInt128 alignment requirement to 8 in system type layout overrides. |
| src/coreclr/tools/Common/Compiler/Int128FieldLayoutAlgorithm.cs | Routes wasm32 through the “32-bit metadata layout” path to match VM layout and avoid 16-byte override. |
| // 32bit platforms use standard metadata layout engine | ||
| if (defType.Context.Target.Architecture == TargetArchitecture.ARM) | ||
| if (defType.Context.Target.Architecture is TargetArchitecture.ARM or TargetArchitecture.Wasm32) | ||
| { |
There was a problem hiding this comment.
Correct that the comment is imprecise — X86 is 32-bit and does take the 16-byte override. That imprecision predates this change though, and the condition it describes is right there on the next line, so I'd rather not expand it here: rewording a pre-existing comment is unrelated churn, and per repo convention comments are kept minimal because inaccurate ones mislead and need maintenance.
Happy to fix it if a maintainer would prefer.
Note
This comment was generated by GitHub Copilot.
| if (defType.Context.Target.Architecture is TargetArchitecture.ARM or TargetArchitecture.Wasm32) | ||
| { | ||
| layoutFromMetadata.LayoutAbiStable = true; | ||
| layoutFromMetadata.IsInt128OrHasInt128Fields = true; |
There was a problem hiding this comment.
Added Wasm32 cases to TestAlignmentBehavior_AutoAlignmentRulesWithOSDependence in 5fe365d. Both fail without the layout change (alignment 16 rather than 8), so they actually guard the new branch rather than just documenting it.
Note
This comment was generated by GitHub Copilot.
Covers the new Wasm32 branch in Int128FieldLayoutAlgorithm alongside the existing ARM/ARM64/X86/X64 cases. Both new cases fail without the layout change (alignment 16 rather than 8). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
| // Wasm has no 128-bit scalar type to match, so use the natural field alignment. | ||
| pLayout->SetAlignmentRequirement(8); | ||
| #else | ||
| #error Unknown architecture | ||
| #endif // TARGET_64BIT |
This impacts packing and layout, something we really need to have correct from v1 as it can be breaking (behavioral, impact interop, invalidate R2R images, etc) to change it later. The general tooling docs describing the basic C ABI (not required, just existing tooling convention) explicitly calls out 16: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#data-representation
Rust notably had a similar bug where they didn't use 16-byte alignment as expected and had to fix it last year: rust-lang/rust#133991 So I don't think we can take this and rather need to ensure the interpreter correctly handles this for stack data. |
|
we have the same problem for v256/512, for which there are no intrinsic types either |
Those ones will require every ecosystem to take a break though, if WASM adds official types in the future. As of now, the layout/alignment should be correct as they should be recognized as 2 or 4x V128 fields I think it's quite a bit different from Is getting the interpreter to handle the 16-byte stack alignment required here (and for |
|
You're right, and the BasicCABI table refutes my reasoning rather than just outweighing it. The row is:
My argument was "there is no corresponding wasm value type, therefore no ABI basis for the alignment override." The spec assigns alignment 16 to a type whose value type is explicitly On the interpreter question: the interpreter looks like the side that's already correct. Tracing the original failure, it placed the Teaching That divergence is reachable rather than theoretical, since Separately, the signature round-trip loses alignment for the vector types too, and that one is independent of whatever we decide for Thunk signatures read out of the R2R image show the collision — Agreed no ecosystem contract is at stake for those yet. Worth noting the alignment isn't currently what you'd expect from 2x/4x V128 though: wasm has no branch in the Note This comment was generated by GitHub Copilot. |
it requires another type signature I think, probably not huge but let me look. |
My point was that they are also currently broken and have 32/64 alignment which probably doesn't make sense for the current wasm abi. So the question becomes how should we pass them? multiple v128s, or as a struct? |
They are defined as So a struct containing 2x V128 fields and a struct containing 2x structs, each containing 2x V128 fields. -- I expect WASM treats this the same as a struct containing 2x and 4x V128 fields, respectively. But some ABIs and our own layout algorithm/recursion should already be taking that into account when reporting things back. |
Right, I'm telling you the current handling is active broken because there is an alignment mismatch. |
|
Right, I got that. I was explaining what the expected behavior is. If its incorrect today, then we probably need to fix it (I have no preference on whether that's the same or a separate PR) |
|
Working on a separate prototype that can express the alignment and decomposition that matches the abi in the call signature without adding entirely new arg types. |
Int128/UInt128were given a 16-byte alignment requirement on wasm by both the VM and crossgen2, annotated// sizeof(v128).Int128is not a v128 — the only 128-bit value type in WebAssembly isv128, andWasmLowering.IsWasmV128Typerecognizes onlyVector128<T>and a 128-bitVector<T>, soInt128uses the generic by-reference struct ABI.Every other alignment override in
CheckForSystemTypesexists because the managed type corresponds to a fundamental data type in the target ABI (Vector128<T>↔__m128, and so on).Int128has no counterpart on wasm, so it takes ordinary struct alignment — the 8 its twoulongfields produce. On the crossgen2 side that means routing wasm through the existing 32-bit path alongside ARM.Symptom
The 16 was observable as wrong codegen at the interpreter→R2R boundary:
Root cause
Wasm signature encoding spells a by-reference struct argument as
S<N>, andRaiseSignatureresolvesS16throughGetCachedStructOfSize, which keys only on size and keeps the first struct seen at that size. The thunk therefore used that struct's 8-byte alignment while the interpreter usedInt128's 16, placing the argument atargsBase+24instead ofargsBase+32.EqualsandCompareToread a shifted value;operator ==takes its operands differently and was unaffected.This also makes
S<N>sound rather than accidentally correct. Single-field structs wrapping a v128 are unwrapped byLowerTypeand encode asV, and a v128 field is the only source of 16-byte alignment on wasm — so onceInt128takes its natural alignment, nothing reachingS<N>requires more than 8.The VM and crossgen2 must change together or managed field layout desyncs.
Validation
Browser wasm, against builds with and without the change, rebuilding both
corerunand the R2R composite for each variant:Equals=False,CompareTo=±1on equal valuesSystem.Text.Json, theInt128-bearing classes, 1496 tests, run on a branch carrying the other outstanding wasm R2R fixes so the suite completes:Those 2 fail with R2R off in both arms — pre-existing and unrelated.
Microsoft.Bcl.Memory550/550 andSystem.Runtime.CompilerServices.Unsafe128/128, R2R on and off.Alternative considered
Encoding
Int128asVinstead does not work.Vis not an alignment tag — it selects a different calling convention, passing the value in a wasmv128local rather than by reference. The JIT has no notion ofInt128at all, so R2R code faults withRuntimeError: function signature mismatch. Confirmed experimentally.clangon wasm32 agrees with the by-reference treatment: it legalizes__int128into twoi64parameters rather than any 128-bit entity, and passes an equivalent 16-byte struct asbyval align 8._Alignof(__int128) == 16there is a memory-layout artifact, not a calling-convention requirement, since there is no 128-bit slot to align to.wasm-only by construction — the VM change is inside
#elif defined(TARGET_WASM)and the crossgen2 change addsTargetArchitecture.Wasm32to an existing condition.Note
This pull request was created with the assistance of GitHub Copilot.